package org.egrit.image;

import java.awt.Point;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.base.Predicate;
import com.vividsolutions.jts.geom.Polygon;

import org.egrit.GridBoundsFactory;
import org.egrit.GridBoundsIterator;
import org.egrit.GridGeometryIterator;
import org.egrit.IGridBounds;
import org.egrit.IGridIterator;
import org.egrit.IGridListener;
import org.egrit.IWritableGrid;
import org.egrit.util.Any;
import org.egrit.util.Args;

import org.jaitools.imageutils.ImageUtils;
import org.jaitools.imageutils.iterator.WritableSimpleIterator;

/**
 * A writable grid backed by a {@linkplain RenderedImage}.
 *
 * @author michael
 */
public class WritableImageBackedGrid extends ImageBackedGrid implements IWritableGrid {
    private final WritableSimpleIterator writeIter;
    
    private final Lock lock;
    
    /**
     * Creates a new raster map, initially filled with {@code fillValue}. Data
     * are stored in a {@linkplain RenderedImage} with the data type inferred
     * from {@code fillValue}.
     * 
     * @param width number of columns
     * @param height number of rows
     * @param fillValue initial fill value (must not be {@code null})
     * 
     * @throws IllegalArgumentException if {@code fillValue} is {@code null}
     */
    WritableImageBackedGrid(int width, int height, Number fillValue, Number outsideValue) {
        super(ImageUtils.createConstantImage(width, height, fillValue), outsideValue);

        this.writeIter = new WritableSimpleIterator(
                (WritableRenderedImage) backingImage, null, outsideValue);
        
        this.lock = new ReentrantLock();
    }

    WritableImageBackedGrid(IGridBounds gridBounds, Number fillValue, Number outsideValue) {
        super(ImageUtils.createConstantImage(
                gridBounds.getMinGridX(), gridBounds.getMinGridY(), 
                gridBounds.getWidth(), gridBounds.getHeight(), fillValue), 
                outsideValue);
        
        this.writeIter = new WritableSimpleIterator(
                (WritableRenderedImage) backingImage, null, outsideValue);
        
        this.lock = new ReentrantLock();
    }

    @Override
    public void lock() {
        lock.lock();
    }

    @Override
    public void unlock() {
        lock.unlock();
    }

    @Override
    public Number getGridValue(int gridX, int gridY) {
        lock.lock();
        try {
            return super.getGridValue(gridX, gridY);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public Number getGridValue(Point gridPos) {
        lock.lock();
        try {
            return super.getGridValue(gridPos);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean setGridValue(int gridX, int gridY, Number value) {
        lock.lock();
        try {
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            
            boolean result = writeIter.setSample(gridX, gridY, value);
            if (result) {
                fireValueChangedEvent(GridBoundsFactory.create(gridX, gridY));
            }
            return result;
            
        } finally {
            lock.unlock();
        }
    }
    
    @Override
    public boolean setGridValue(Point gridPos, Number value) {
        if (gridPos == null) {
            throw new IllegalArgumentException("gridPos must not be null");
        }
        return setGridValue(gridPos.x, gridPos.y, value);
    }

    @Override
    public boolean setGridValue(int gridX, int gridY, Number value, Predicate<Number> cond) {
        lock.lock();
        try {
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            if (cond == null) {
                throw new IllegalArgumentException("the value condition must not be null");
            }
            
            if (isWithin(gridX, gridY) && cond.apply(writeIter.getSample(gridX, gridY))) {
                boolean result = writeIter.setSample(gridX, gridY, value);
                if (result) {
                    fireValueChangedEvent(GridBoundsFactory.create(gridX, gridY));
                }
                return result;
            } else {
                return false;
            }
            
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int setGridValue(IGridBounds bounds, Number value) {
        lock.lock();
        try {
            Args.notNull("bounds", bounds, "value", value);
            
            if (bounds.isEmpty()) {
                return 0;
            }
            
            IGridIterator iter = new GridBoundsIterator(bounds);
            int count = 0;
            do {
                if (writeIter.setSample(iter.getPos(), value)) {
                    count++;
                }
                
            } while (iter.next());
            
            if (count > 0) {
                fireValueChangedEvent(GridBoundsFactory.create(bounds));
            }
            return count;
            
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int setGridValue(IGridBounds bounds, Number value, Predicate<Number> cond) {
        lock.lock();
        try {
            Args.notNull("bounds", bounds, "value", value, "cond", cond);
            
            if (bounds.isEmpty()) {
                return 0;
            }
            
            // A flag to avoid unnecessary grid reads if the condition is ANY_VALUE
            final boolean ACCEPT_ANY = cond instanceof Any;
            
            IGridIterator iter = new GridBoundsIterator(bounds);
            int count = 0;
            do {
                java.awt.Point pos = iter.getPos();
                if ( (ACCEPT_ANY || cond.apply(writeIter.getSample(pos)))
                        && writeIter.setSample(pos, value)) {
                    count++;
                }
                
            } while (iter.next());
            
            if (count > 0) {
                fireValueChangedEvent(GridBoundsFactory.create(bounds));
            }
            return count;
            
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int setGridValue(Polygon poly, Number value) {
        return setGridValue(poly, value, new Any<Number>());
    }
    
    @Override
    public int setGridValue(Polygon poly, Number value, Predicate<Number> cond) {
        lock.lock();
        try {
            Args.notNull("poly", poly, "value", value, "cond", cond);
            
            // A flag to avoid unnecessary grid reads if the condition is ANY_VALUE
            final boolean ANY_VALUE = cond instanceof Any;
            
            int count = 0;
            IGridBounds workingBounds = GridBoundsFactory.create(poly).intersection(bounds);
            if (!workingBounds.isEmpty()) {
                IGridIterator iter = new GridGeometryIterator(poly, bounds);
                do {
                    java.awt.Point pos = iter.getPos();
                    if ((ANY_VALUE || cond.apply(writeIter.getSample(pos)))
                            && writeIter.setSample(pos, value)) {
                        count++;
                    }

                } while (iter.next());
            }
            
            if (count > 0) {
                fireValueChangedEvent(workingBounds);
            }
            
            return count;
            
        } finally {
            lock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Note: the new map holds a copy of this map's data so subsequent changes to 
     * this map will not affect the new map and vice versa.
     */
    @Override
    public IWritableGrid getWritableRegion(IGridBounds regionBounds) {
        lock.lock();
        try {
            if (regionBounds == null || regionBounds.isEmpty()) {
                throw new IllegalArgumentException("region bounds cannot be null or empty");
            }

            WritableImageBackedGrid regionMap = new WritableImageBackedGrid(regionBounds, outsideValue, outsideValue);

            if (regionBounds.intersects(this.bounds)) {
                IGridIterator iter = regionBounds.getIterator();
                do {
                    Point pos = iter.getPos();
                    regionMap.setGridValue(pos, getGridValue(pos));
                } while (iter.next());
            }
            
            return regionMap;
            
        } finally {
            lock.unlock();
        }
    }

    private void fireValueChangedEvent(IGridBounds boundsOfChange) {
        for (IGridListener listener : listeners) {
            listener.valueChanged(boundsOfChange);
        }
    }

}
